/*
 * Copyright (C) 2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Logging;
using DCSharp.Settings;

namespace DCSharp.Backend.Managers
{
	public class UploadManager : IEnumerable<Uploader>
	{
		public event EventHandler<UploadEventArgs> UploadStarted;
		public event EventHandler<UploadEventArgs> UploadCompleted;
		public event EventHandler<UploadEventArgs> UploadAborted;

		private Logger log = LogManager.GetLogger("UploadManager");

		private IRuntimeSettings settings;
		//private ConnectionManager connectionManager;
		private ShareManager shareManager;

		private Dictionary<User, UserConnection> slots;
		private Dictionary<UserConnection, Uploader> uploads;

		public UploadManager(IRuntimeSettings settings,
			ConnectionManager connectionManager, ShareManager shareManager)
		{
			if (settings == null)
			{
				throw new ArgumentNullException("settings");
			}
			if (connectionManager == null)
			{
				throw new ArgumentNullException("connectionManager");
			}
			if (shareManager == null)
			{
				throw new ArgumentNullException("shareManager");
			}
			this.settings = settings;
			//this.connectionManager = connectionManager;
			this.shareManager = shareManager;

			slots = new Dictionary<User, UserConnection>();
			uploads = new Dictionary<UserConnection, Uploader>();

			// Events
			connectionManager.UserConnected += OnUserConnected;
			connectionManager.UserDisconnected += OnUserDisconnected;
		}

		#region Properties

		public int FreeSlots
		{
			get
			{
				int free = settings.UploadSlots ?? int.MaxValue - slots.Count;
				return free > 0 ? free : 0;
			}
		}

		public object SyncRoot
		{
			get { return uploads; }
		}

		#endregion

		#region Methods

		public IEnumerator<Uploader> GetEnumerator()
		{
			return uploads.Values.GetEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		protected bool GetSlot(UserConnection connection, Request request)
		{
			log.Info("Handling request " + request + " from " + connection.RemoteUser);
			if (!connection.RemoteUser.IsOnline)
			{
				log.Info("Denying upload request. User offline.", null,
					connection.RemoteUser, request);
				return false;
			}
			if (slots.ContainsValue(connection))
			{
				return true;
			}

			// MiniSlots support
			FileRequest fileRequest = request as FileRequest;
			if (request is ListRequest ||
				(fileRequest != null && fileRequest.Count != null &&
				fileRequest.Count < 16 * 1024))
			{
				return true;
			}

			lock (slots)
			{
				if (slots.ContainsKey(connection.RemoteUser) &&
					!slots.ContainsValue(connection))
				{
					log.Info("Denying upload request. Existing connection found.",
						null, connection.RemoteUser, request);
					throw new NoSlotsException();
				}

				int? max = settings.UploadSlots;
				if (max != null && slots.Count >= max)
				{
					log.Info("Denying upload request. All slots full.", null,
						connection.RemoteUser, request);
					throw new NoSlotsException();
				}

				// Give the user a slot
				if (!slots.ContainsKey(connection.RemoteUser))
				{
					slots.Add(connection.RemoteUser, connection);
				}
				return true;
			}
		}

		protected virtual void OnUploadStarted(Uploader uploader)
		{
			Debug.Assert(uploader.Active);
			if (UploadStarted != null)
			{
				UploadStarted(this, new UploadEventArgs(uploader));
			}
		}

		protected virtual void OnUploadCompleted(Uploader uploader)
		{
			if (UploadCompleted != null)
			{
				UploadCompleted(this, new UploadEventArgs(uploader));
			}
		}

		protected virtual void OnUploadAborted(Uploader uploader)
		{
			if (UploadAborted != null)
			{
				UploadAborted(this, new UploadEventArgs(uploader));
			}
		}

		private void DisposeUploader(UserConnection connection)
		{
			lock (uploads)
			{
				Uploader uploader;
				if (uploads.TryGetValue(connection, out uploader))
				{
					uploads.Remove(connection);

					uploader.ActiveChanged -= OnActiveChanged;
					uploader.Dispose();
				}
			}
		}

		#region Event Handlers

		private void OnUserConnected(object obj, ConnectionEventArgs args)
		{
			UserConnection connection = (UserConnection)args.Connection;

			connection.UploadRequest += OnUploadRequest;
		}

		private void OnUserDisconnected(object obj, ConnectionEventArgs args)
		{
			UserConnection connection = (UserConnection)args.Connection;

			connection.UploadRequest -= OnUploadRequest;

			DisposeUploader(connection);
			lock (slots)
			{
				if (slots.ContainsValue(connection))
				{
					slots.Remove(connection.RemoteUser);
				}
			}
		}

		private void OnUploadRequest(object obj, UploadRequestEventArgs args)
		{
			UserConnection connection = (UserConnection)obj;
			if (args.Response == null && GetSlot(connection, args.Request))
			{
				// TODO: Reuse the uploader if the user is requesting multiple
				// segments of a file.
				FileRequest fileRequest = args.Request as FileRequest;
				FileRequest oldRequest;

				// Check if this request matches the old request
				Uploader uploader;
				if (fileRequest != null &&
					uploads.TryGetValue(connection, out uploader))
				{
					oldRequest = uploader.Request as FileRequest;
					if (oldRequest != null && (
						(fileRequest.TTH != null && fileRequest.TTH == oldRequest.TTH) ||
						(fileRequest.Path != null && fileRequest.Path == oldRequest.Path)))
					{
						// Reuse the uploader
						uploader.HandleUploadRequest(connection, args);
						return;
					}
				}

				// Create a new uploader
				uploader = new Uploader(shareManager, connection);
				if (uploader.HandleUploadRequest(connection, args))
				{
					uploader.ActiveChanged += OnActiveChanged;

					DisposeUploader(connection);
					lock (uploads)
					{
						uploads[connection] = uploader;
					}
				}
			}
		}

		private void OnActiveChanged(object obj, EventArgs args)
		{
			Uploader uploader = (Uploader)obj;
			if (uploader.Active)
			{
				OnUploadStarted(uploader);
			}
			else if (uploader.Progress < 1)
			{
				OnUploadAborted(uploader);
			}
			else
			{
				OnUploadCompleted(uploader);
			}
		}

		#endregion

		#endregion
	}
}
